/*!
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {DirectiveOwner, ParseSourceSpan, TmplAstHostElement} from '@angular/compiler';
import ts from 'typescript';
import type {Context} from './context';
import type {Scope} from './scope';
import {TcbOp} from './base';
import {TypeCheckableDirectiveMeta} from '../../api';
import {Reference} from '../../../imports';
import {ClassDeclaration} from '../../../reflection';
import {addExpressionIdentifier, ExpressionIdentifier} from '../comments';
import {addParseSpanInfo} from '../diagnostics';
import {tsDeclareVariable} from '../ts_util';

/**
 * A `TcbOp` which constructs an instance of a directive. For generic directives, generic
 * parameters are set to `any` type.
 */
export abstract class TcbDirectiveTypeOpBase extends TcbOp {
  constructor(
    protected tcb: Context,
    protected scope: Scope,
    protected node: DirectiveOwner,
    protected dir: TypeCheckableDirectiveMeta,
  ) {
    super();
  }

  override get optional() {
    // The statement generated by this operation is only used to declare the directive's type and
    // won't report diagnostics by itself, so the operation is marked as optional to avoid
    // generating declarations for directives that don't have any inputs/outputs.
    return true;
  }

  override execute(): ts.Identifier {
    const dirRef = this.dir.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>;

    const rawType = this.tcb.env.referenceType(this.dir.ref);

    let type: ts.TypeNode;
    let span: ParseSourceSpan;
    if (this.dir.isGeneric === false || dirRef.node.typeParameters === undefined) {
      type = rawType;
    } else {
      if (!ts.isTypeReferenceNode(rawType)) {
        throw new Error(
          `Expected TypeReferenceNode when referencing the type for ${this.dir.ref.debugName}`,
        );
      }
      const typeArguments = dirRef.node.typeParameters.map(() =>
        ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
      );
      type = ts.factory.createTypeReferenceNode(rawType.typeName, typeArguments);
    }

    if (this.node instanceof TmplAstHostElement) {
      span = this.node.sourceSpan;
    } else {
      span = this.node.startSourceSpan || this.node.sourceSpan;
    }

    const id = this.tcb.allocateId();
    addExpressionIdentifier(id, ExpressionIdentifier.DIRECTIVE);
    addParseSpanInfo(id, span);
    this.scope.addStatement(tsDeclareVariable(id, type));
    return id;
  }
}

/**
 * A `TcbOp` which constructs an instance of a non-generic directive _without_ setting any of its
 * inputs. Inputs are later set in the `TcbDirectiveInputsOp`. Type checking was found to be
 * faster when done in this way as opposed to `TcbDirectiveCtorOp` which is only necessary when the
 * directive is generic.
 *
 * Executing this operation returns a reference to the directive instance variable with its inferred
 * type.
 */
export class TcbNonGenericDirectiveTypeOp extends TcbDirectiveTypeOpBase {
  /**
   * Creates a variable declaration for this op's directive of the argument type. Returns the id of
   * the newly created variable.
   */
  override execute(): ts.Identifier {
    const dirRef = this.dir.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>;
    if (this.dir.isGeneric) {
      throw new Error(`Assertion Error: expected ${dirRef.debugName} not to be generic.`);
    }
    return super.execute();
  }
}

/**
 * A `TcbOp` which constructs an instance of a generic directive with its generic parameters set
 * to `any` type. This op is like `TcbDirectiveTypeOp`, except that generic parameters are set to
 * `any` type. This is used for situations where we want to avoid inlining.
 *
 * Executing this operation returns a reference to the directive instance variable with its generic
 * type parameters set to `any`.
 */
export class TcbGenericDirectiveTypeWithAnyParamsOp extends TcbDirectiveTypeOpBase {
  override execute(): ts.Identifier {
    const dirRef = this.dir.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>;
    if (dirRef.node.typeParameters === undefined) {
      throw new Error(
        `Assertion Error: expected typeParameters when creating a declaration for ${dirRef.debugName}`,
      );
    }

    return super.execute();
  }
}
